﻿//-----------------------------------------------------------------------
// <copyright file="SequentialGuid.cs" company="Jeremy H. Todd">
//     Copyright © Jeremy H. Todd 2011
// </copyright>
//-----------------------------------------------------------------------

using System;
using System.Security.Cryptography;

// ReSharper disable once CheckNamespace
namespace System
{
    /// <summary>
    /// Contains methods for creating sequential GUID values.
    /// 生成连续有意义的GUID。
    /// </summary>
    public static class SequentialGuid
    {
        #region Static Fields

        /// <summary>
        /// Provides cryptographically strong random data for GUID creation.
        /// </summary>
        private static readonly RNGCryptoServiceProvider _RandomGenerator = new RNGCryptoServiceProvider();

        #endregion

        private static int _TimeOffset = 2;

        /// <summary>
        /// Returns a new GUID value which is sequentially ordered when formatted as
        /// a string, a byte array, or ordered by the least significant six bytes of the
        /// Data4 block, as specified by <paramref name="guidType" />.
        /// </summary>
        /// <param name="guidType">
        /// Specifies the type of sequential GUID (i.e. whether sequential as a string,
        /// as a byte array, or according to the Data4 block.  This can affect
        /// performance under various database types; see below.
        /// </param>
        /// <returns>
        /// A <see cref="Guid" /> structure whose value is created by replacing
        /// certain randomly-generated bytes with a sequential timestamp.
        /// </returns>
        /// <remarks>
        /// <para>
        /// This method creates a new GUID value which combines a random component
        /// with the current timestamp, also known as a COMB.  The general concept
        /// is outlined in Jimmy Nilsson's article "The Cost of GUIDs as Primary Keys",
        /// and involves replacing either the least significant or most significant
        /// six bytes of the GUID with the current timestamp.  This reduces the
        /// random component of the GUID from 16 bytes to 10 bytes, but this is
        /// still sufficient to prevent a collision under most real-world circumstances.
        /// </para>
        /// <para>
        /// The purpose of sequential GUIDs is not to promote the use of GUIDs as
        /// sortable entities.  In fact, GUIDs generated very close together may
        /// have the same timestamp and are not guaranteed to be sequentially ordered
        /// at all.  The intent is to increase performance when doing repeated
        /// inserts into database tables that have a clustered index on a GUID
        /// column, so that later entries do not have to be inserted into the middle
        /// of the table, but can simply be appended to the end.
        /// </para>
        /// <para>
        /// According to experiments, Microsoft SQL Server sorts GUID values using
        /// the least significant six bytes of the Data4 block; therefore, GUIDs being
        /// generated for use with SQL Server should pass a <paramref name="guidType" /> 
        /// value of <c>SequentialAtEnd</c>.  GUIDs generated for most other database
        /// types should be passed a <paramref name="guidType" /> value of
        /// <c>SequentialAsString</c> or <c>SequentialAsByteArray</c>.
        /// </para>
        /// <para>
        /// Various standards already define a time-based UUID; however, the
        /// format specified by these standards splits the timestamp into
        /// several components, limiting its usefulness as a sequential ID.
        /// Additionally, the format used for such UUIDs is not compatible
        /// with the GUID ordering on Microsoft SQL Server.
        /// </para>
        /// </remarks>
        public static Guid Create(SequentialGuidType guidType = SequentialGuidType.SequentialAsString)
        {
            // We start with 16 bytes of cryptographically strong random data.
            byte[] randomBytes = new byte[10];
            _RandomGenerator.GetBytes(randomBytes);
            // An alternate method: use a normally-created GUID to get our initial
            // random data:
            // byte[] randomBytes = Guid.NewGuid().ToByteArray();
            // This is faster than using RNGCryptoServiceProvider, but I don't
            // recommend it because the .NET Framework makes no guarantee of the
            // randomness of GUID data, and future versions (or different
            // implementations like Mono) might use a different method.

            // Now we have the random basis for our GUID.  Next, we need to
            // create the six-byte block which will be our timestamp.

            // We start with the number of milliseconds that have elapsed since
            // DateTime.MinValue.  This will form the timestamp.  There's no use
            // being more specific than milliseconds, since DateTime.Now has
            // limited resolution.

            // Using millisecond resolution for our 48-bit timestamp gives us
            // about 5900 years before the timestamp overflows and cycles.
            // Hopefully this should be sufficient for most purposes. :)
            long timestamp = DateTime.UtcNow.Ticks / 10000L;

            // Then get the bytes
            byte[] timestampBytes = BitConverter.GetBytes(timestamp);

            // Since we're converting from an Int64, we have to reverse on Little-Endian systems.
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(timestampBytes);
            }

            byte[] guidBytes = new byte[16];

            switch (guidType)
            {
                case SequentialGuidType.SequentialAsString:
                case SequentialGuidType.SequentialAsBinary:

                    // For string and byte-array version, we copy the timestamp first, followed
                    // by the random data.
                    Buffer.BlockCopy(timestampBytes, _TimeOffset, guidBytes, 0, 6);
                    Buffer.BlockCopy(randomBytes, 0, guidBytes, 6, 10);

                    // If formatting as a string, we have to compensate for the fact
                    // that .NET regards the Data1 and Data2 block as an Int32 and an Int16,
                    // respectively.  That means that it switches the order on little-endian
                    // systems.  So again, we have to reverse.
                    if (guidType == SequentialGuidType.SequentialAsString && BitConverter.IsLittleEndian)
                    {
                        Array.Reverse(guidBytes, 0, 4);
                        Array.Reverse(guidBytes, 4, 2);
                    }

                    break;

                case SequentialGuidType.SequentialAtEnd:

                    // For sequential-at-the-end versions, we copy the random data first,
                    // followed by the timestamp.
                    Buffer.BlockCopy(timestampBytes, _TimeOffset, guidBytes, 10, 6);
                    Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 10);
                    break;
            }

            return new Guid(guidBytes);
        }

        public static long ExtractDateTimeTicks(Guid guid)
        {
            return ExtractDateTimeTicks(guid.ToByteArray());
        }

        public static long ExtractDateTimeTicks(string guid)
        {
            return !Guid.TryParse(guid, out var id)
                ? throw new ArgumentException("The incoming parameter is not a GUID string!", nameof(guid))
                : ExtractDateTimeTicks(id);
        }

        public static long ExtractDateTimeTicks(byte[] guidBytes, SequentialGuidType guidType = SequentialGuidType.SequentialAsString)
        {
            byte[] timestampBytes = new byte[8];
            switch (guidType)
            {
                case SequentialGuidType.SequentialAsString:
                case SequentialGuidType.SequentialAsBinary:
                    Buffer.BlockCopy(guidBytes, 0, timestampBytes, 2, 4);
                    break;
                case SequentialGuidType.SequentialAtEnd:
                    Buffer.BlockCopy(guidBytes, 2, timestampBytes, 12, 4);
                    break;
            }
            return BitConverter.ToInt64(timestampBytes, 0);
        }
    }


    /// <summary>
    /// Describes the type of a sequential GUID value.
    /// </summary>
    public enum SequentialGuidType
    {
        /// <summary>
        /// The GUID should be sequential when formatted using the
        /// <see cref="Guid.ToString()" /> method.
        /// </summary>
        SequentialAsString,

        /// <summary>
        /// The GUID should be sequential when formatted using the
        /// <see cref="Guid.ToByteArray" /> method.
        /// </summary>
        SequentialAsBinary,

        /// <summary>
        /// The sequential portion of the GUID should be located at the end
        /// of the Data4 block.
        /// </summary>
        SequentialAtEnd
    }
}